RPC(大点数脉冲压缩算法)

1. 概述

本文将以基于 FT04-DSP 的 rpc 算法的移植为例,介绍 AI+DSP 应用开发的开发流程。

算法简介:

大点数脉冲压缩(Pulse Compression)是雷达信号处理中的关键技术,该算法通过匹配滤波将宽脉冲信号压缩成窄脉冲,能够提高雷达的距离分辨率和检测能力,广泛用于气象雷达、合成孔径雷达、医学成像、地震勘探等需要处理超大点数信号的领域。

2. FT04 c实现

主要代码如下

void main(void)
{
	int ID = getcoreid();
	int iCntI, iCntJ;			/**循环计数*/
	int mask = 15;				// 核掩码,低四位有效,0表示核0执行,3表示核0、核1执行,mask表示四个核执行
	int iHeaderCPLX = 2048 / 8; /**回波协议*/
	int iReadLenPerCore, iReadStPerCore, iWriteLenPerCore, iWriteStPerCore;
	int iFillZeroLen;
	int iPingPongFlag = 0;

	int iChannelNum = 5;				   /**总波束个数,也就是通道个数*/
	int iImageValidRLen = 43456;		   /**距离向有效点数*/
	int iRSampleLen = 43456;			   /**距离向采样长度*/
	int iASampleLen = 17;				   /**方位向收数点数*/
	int iRpcLength = 65536;				   /**脉压结果长度*/
	int iTpADNum;						   /**脉冲宽度Tp内的采样点数*/
	float fFs = 25;						   /**采样率MHz*/
	float fTp = 210;					   /**脉冲宽度us*/
	float fKr = 20.2775 / 210;			   /**调频斜率*/
	float fLosVel = 1.485044537103641e+03; /**平台和目标沿雷达视线的速度分量,单位m/s*/
	float fPRT = 2.38092e+03;			   /**发射脉冲重复间隔,单位us*/
	float fDr = 5.99584916;				   /**距离单元宽度*/
	float fTemp0;
	float r, theta;

	iTpADNum = (int)(fFs * fTp + 0.5); /**脉冲宽度Tp内的采样点数*/
	iReadLenPerCore = iRSampleLen >> 2;
	iReadStPerCore = iReadLenPerCore * ID;
	iWriteLenPerCore = iImageValidRLen >> 2;
	iWriteStPerCore = iWriteLenPerCore * ID;
	iFillZeroLen = iRpcLength - iRSampleLen;

	CPLX *pcDDRfftTwid = (CPLX *)0x140000000;  /**旋转因子 需要多少地址空间?*/
	CPLX *pcDDRifftTwid = (CPLX *)0x180000000; /**旋转因子 需要多少地址空间?*/
	float *pfGSMAngle = (float *)0x70320000;
	CPLX *pcGSMData1 = (CPLX *)(0x70330000);
	CPLX *pcGSMData2 = (CPLX *)(0x703b0000);
	CPLX *pcGSMfftBuf = (CPLX *)(0x70200000); /**算子缓存地址,所需空间为数据量的2倍,计算后释放*/
	CPLX *pcGSMBuf = (CPLX *)(0x703a0000);	  /**缓存地址*/
	CPLX *pcGSMRefBuf = (CPLX *)(0x70760000); /**运动补偿系数(方位线)缓存地址*/
	CPLX *pcGSMPcRef = (CPLX *)(0x70430000);  /**参考函数*/
	CPLX *pcGSMRcRef = (CPLX *)(0x704b0000);  /**运动补偿系数*/
	CPLX *pcGSMRef = (CPLX *)(0x70730000);	  /**运动补偿系数*/
	CPLX *pcGSMDataIn[2];
	CPLX *pcGSMDataOut[2];
	CPLX *pcDDRDataIn = (CPLX *)(0x80000000);	/**原始回波存储地址*/
	CPLX *pcDDRDataOut = (CPLX *)(0x100000000); /**结果存储地址*/
	int *piGSMBuf = (int *)(0x70780000);
	pcGSMDataIn[0] = (CPLX *)(0x70530000);
	pcGSMDataIn[1] = (CPLX *)(0x705b0000);
	pcGSMDataOut[0] = (CPLX *)(0x70630000);
	pcGSMDataOut[1] = (CPLX *)(0x706b0000);

	unsigned long long cnt, cnt1, Use_Time, cnt2, cnt3, Use_Time1, cnt4, cnt5, Use_Time2, cnt6, cnt7, cnt8, Use_Time3, Use_Time4, Use_Time5, Use_Time6, Use_Time7;
	setTimerPeriod(0xffffffff); // 配置定时器周期0xffffffff
	configTimer(0x80);			// 持续性计数,定时器当计数等于周期后一个时钟周期,计数重置为0再重新计数
	asm("smfence\t");
	timerStart(0);
	asm("smfence\t");

	sys_bar(0, 4);
	cnt = getTimerCount();
	int i;
	float *p;

	/**旋转因子初始化*/
	gk_getw_core((CPLX *)pcDDRfftTwid, iRpcLength, 0, mask);
	sys_bar(0, 4);
	gk_getw_core((CPLX *)pcDDRifftTwid, iRpcLength, 1, mask);
	sys_bar(0, 4);
	/**计算脉压系数*/
	get_angle_f_core(fFs, fKr, iTpADNum, pfGSMAngle, mask);
	sys_bar(0, 4);
	gk_cos_sin_f_core(pfGSMAngle, iTpADNum, pcGSMData1, mask);
	sys_bar(0, 4);
	gk_cvmemfill_f_core(0, 0, iRpcLength - iTpADNum, pcGSMData1 + iTpADNum, mask);
	sys_bar(1, 4);
	gk_getw_core((CPLX *)pcDDRfftTwid, iRpcLength, 0, mask);
	sys_bar(0, 4);
	gk_FFT_core((CPLX *)pcGSMData1, (CPLX *)pcDDRfftTwid, (CPLX *)pcGSMData2, iRpcLength, (CPLX *)pcGSMfftBuf, mask); /**FFT*/
	sys_bar(12, 4);
	gk_bitrev_core((float *)pcGSMData2, iRpcLength, (float *)pcGSMPcRef, (float *)pcGSMfftBuf, mask); /**反序 输出为pcGSMRef*/
	sys_bar(3, 4);
	gk_cvconj_f_core((float *)pcGSMPcRef, iRpcLength, (float *)pcGSMPcRef, mask);
	sys_bar(4, 4);
	gk_cvmemfill_f_core(1, 0, iRpcLength, pcGSMRcRef, mask);
	sys_bar(0, 4);

	/**运动补偿初始化*/
	gk_cvmemfill_f_core(0, 1, iASampleLen, pcGSMRefBuf, mask);
	sys_bar(0, 4);
	fTemp0 = -fLosVel * fPRT * 1e-6 / fDr / iRpcLength;									/**(velLos*prt*(noPulse-1)+rangeComp/2)/widthRange/numFFT*/
	gk_tmuls_f_4core(-(iASampleLen - 1) / 2.0, fTemp0, iASampleLen, (float *)pcGSMBuf); /**rcCoef=exp(1j * 2 * pi * (-losVel*pri*((0:(prNum-1))-(prNum-1)/2)/disWidth)/range_fftNum);*/
	sys_bar(0, 4);
	gk_vej_f_4core((float *)pcGSMBuf, pcGSMBuf, iASampleLen, pcGSMRefBuf);
	sys_bar(0, 4);

	/**这一块因为没有直接求int型向量的函数,所以用了两步*/
	gk_tmuls_f_4core(-iRpcLength / 2, 1, iRpcLength, pcGSMRcRef); /**生成距离向向量*/
	sys_bar(0, 4);
	gk_vmuls_i_core(pcGSMRcRef, 1, iRpcLength, piGSMBuf, 15); /**需要int型(做转换)*/
	sys_bar(0, 4);
	/**读取单条脉冲单个通道的数据*/
	pcDDRDataIn = pcDDRDataIn + iHeaderCPLX;
	DMA_Config(6, pcDDRDataIn + iReadStPerCore, pcGSMDataIn[iPingPongFlag] + iReadStPerCore, 0, 0, iReadLenPerCore, 8);
	DMA_start(0x40);
	DMA_check(0x40);
	sys_bar(0, 4);

	pcDDRDataIn = pcDDRDataIn + iRpcLength; // 源数据通道间隔为65536
	cnt1 = getTimerCount();
	Use_Time = (cnt1 - cnt) * 20;
	for (iCntJ = 0; iCntJ < iASampleLen; iCntJ++) /***/
	{
		Use_Time1 = 0;
		Use_Time2 = 0;
		sys_bar(0, 4);
		cnt2 = getTimerCount();

		r = sqrt(pcGSMRefBuf[iCntJ].re * pcGSMRefBuf[iCntJ].re + pcGSMRefBuf[iCntJ].im * pcGSMRefBuf[iCntJ].im);
		theta = atan(pcGSMRefBuf[iCntJ].im / pcGSMRefBuf[iCntJ].re);
		gk_cvpown_f_core(r, theta, iRpcLength, piGSMBuf, pcGSMData2, mask);
		sys_bar(0, 4);

		DMA_Config(0, pcGSMData2 + iRpcLength / 8 * ID, pcGSMRcRef + iRpcLength / 2 + iRpcLength / 8 * ID, 0, 0, 0x8000, 2); /**做一下fftshift*/
		DMA_Config(1, pcGSMData2 + iRpcLength / 2 + iRpcLength / 8 * ID, pcGSMRcRef + iRpcLength / 8 * ID, 0, 0, 0x8000, 2);
		DMA_start(3);
		DMA_check(3);
		sys_bar(0, 4);

		cnt3 = getTimerCount();
		Use_Time1 = (cnt3 - cnt2) * 20 + Use_Time1;
		for (iCntI = 0; iCntI < iChannelNum; iCntI++) /***/
		{
			if (iCntI == (iChannelNum - 1))
			{
				pcDDRDataIn = pcDDRDataIn + iHeaderCPLX;
			}
			sys_bar(0, 4);
			iPingPongFlag = ((iCntJ * iChannelNum) + iCntI + 1) % 2;
			/**读取单条脉冲单个通道的数据*/
			DMA_Config(6, pcDDRDataIn + iReadStPerCore, pcGSMDataIn[iPingPongFlag] + iReadStPerCore, 0, 0, iReadLenPerCore, 8);
			DMA_start(0x40);
			iPingPongFlag = ((iCntJ * iChannelNum) + iCntI) % 2;
			gk_cvmemfill_f_core(0, 0, iFillZeroLen, pcGSMDataIn[iPingPongFlag] + iRSampleLen, mask);
			sys_bar(0, 4);
			gk_FFT_core((CPLX *)pcGSMDataIn[iPingPongFlag], (CPLX *)pcDDRfftTwid, (CPLX *)pcGSMData2, iRpcLength, (CPLX *)pcGSMfftBuf, mask); /**FFT*/
			sys_bar(0, 4);
			gk_bitrev_core((float *)pcGSMData2, iRpcLength, (float *)pcGSMData1, (float *)pcGSMfftBuf, mask); /**反序 输出为pcGSMRef*/
			sys_bar(0, 4);

			gk_cvmulcv_f_core(pcGSMData1, pcGSMRcRef, iRpcLength, pcGSMData1, mask); /**乘运动补偿*/
			sys_bar(0, 4);
			gk_cvmulcv_f_core(pcGSMData1, pcGSMPcRef, iRpcLength, pcGSMData1, mask); /**乘脉压系数*/
			sys_bar(0, 4);

			gk_IFFT_core((CPLX *)pcGSMData1, (CPLX *)pcDDRfftTwid, (CPLX *)pcGSMData2, iRpcLength, (CPLX *)pcGSMfftBuf, mask); /**FFT*/
			sys_bar(0, 4);
			gk_bitrev_core((float *)pcGSMData2, iRpcLength, (float *)pcGSMDataOut[iPingPongFlag], (float *)pcGSMfftBuf, mask); /**反序 输出为pcGSMRef*/
			sys_bar(0, 4);
			/**存储单条脉冲单个通道的数据*/
			if (((iCntJ * iChannelNum) + iCntI) > 0)
			{
				DMA_check(0x80);
			}
			DMA_Config(7, pcGSMDataOut[iPingPongFlag] + iWriteStPerCore, pcDDRDataOut + iWriteStPerCore, 0, 0, iWriteLenPerCore, 8);
			DMA_start(0x80);
			sys_bar(0, 4);
			pcDDRDataIn = pcDDRDataIn + iRpcLength; // 源数据通道间隔为65536
			pcDDRDataOut = pcDDRDataOut + iImageValidRLen;
			DMA_check(0x40);
			cnt4 = getTimerCount();
			Use_Time2 = (cnt4 - cnt3) * 20 + Use_Time2;
		}
	}
	DMA_check(0x80);
	if (ID == 0)
	{
		printf("ok\n\n");
	}
}

本代码用 FT-IDE 编译,能在ft04 板卡上加载、运行。

部分接口介绍:

  • sys_bar 为栅栏接口,用于多核同步:只有当所有核都完成当前计算任务后,才会开启下一个计算任务。

  • getTimerCount 为时钟接口,它返回当前时钟的节拍数:用于性能统计。

  • DMA_Config 为DMA传输接口,用于提升数据的搬移的速度。

  • gk_ 开头的函数为具体的 dsp 算子,算子的功能可通过查阅相关文档、头文件、代码可知,本文就不逐一介绍。

3. 开发流程

3.1 数据导入

c实现中,数据读取是通过IDE导入的。在python中使用 open 函数打开并读取原始的二进制数据文件,并把其转成complex64类型的复数tensor。

示例代码:

with open("data_43456_17_rpcdata_chnum5_RPCDataIn.bin", 'rb') as f:
    raw_data = f.read()
echo = np.frombuffer(raw_data, dtype=np.complex64)
echo = Tensor(echo)

3.2 常量处理

移植时的主要工作是:将 c 算子的计算逻辑转换为 MindSpore Signal+ 的 API 调用。

整体分析可知,从数据导入到内层循环的fft之前的步骤,主要是模型常量的生成,另外乘法算子

gk_cvmulcv_f_core(pcGSMData1, pcGSMRcRef, iRpcLength, pcGSMData1, mask); /**乘运动补偿*/
sys_bar(0, 4);
gk_cvmulcv_f_core(pcGSMData1, pcGSMPcRef, iRpcLength, pcGSMData1, mask); /**乘脉压系数*/

中的 pcGSMRcRefpcGSMPcRef 均为长度为 65536 的一维数据,其生成只依赖于给定的参数,不依赖于原始输入,为模型中的常量。 为避免额外的开销,可将其相乘后放在__init__中。

示例代码:

class rpc(nn.Cell):
    def __init__(self):
        super(rpc, self).__init__()
        fFs = 25
        fTp = 210
        fKr = 20.2775 / 210
        iTpADNum = (int)(fFs * fTp + 0.5)
        iRpcLength = 65536
        ...

3.3 核心计算

循环部分是算法的核心,其主要流程是在循环中对一维的数据做fft、乘法和ifft运算。

可使用下列优化方法来提升性能:

  • 维度提升:因为框架中算子:fft、乘法和ifft均支持二维输入,所以把核心中的一维算子、数据改写成二维的,这样可节省框架的循环调用的开销。

gk_FFT_core((CPLX *)pcGSMDataIn[iPingPongFlag], (CPLX *)pcDDRfftTwid, (CPLX *)pcGSMData2, iRpcLength, (CPLX *)pcGSMfftBuf, mask); /**FFT*/

中的 pcGSMDataIn 为长度为 65536 的一维输入数据,可把其当成一个二维输入 tensor 中的一行。

gk_bitrev_core((float *)pcGSMData2, iRpcLength, (float *)pcGSMDataOut[iPingPongFlag], (float *)pcGSMfftBuf, mask); /**反序 输出为pcGSMRef*/

中的 pcGSMDataOut 为长度为 65536 的一维输出数据,可把其当成一个二维输出 tensor 中的一行。

  • 空间分配:在运行前为框架算子缓存区分配空间,并将存储类型设置为 smc,这样能提升算子的访存速度,对于此应用,使用smc前后运行时间分别为:88ms、70ms,性能提升了 18 ms。

示例代码:

    def construct(self, t):
        t = t.reshape(self.iASampleLen, t.size//self.iASampleLen)                 
        t = self.slice(t, [0, self.iHeaderCPLX], [self.iASampleLen, self.iRpcLength*self.iChannelNum])
        t = t.reshape(self.line, self.iRpcLength)
        t = self.slice(t, [0,0], [self.line, self.iRSampleLen])
        t = ops.cat((t, self.zero), 1)
        t = self.fft(t)
        t = self.mul(t, self.pcGSMRcRef)
        t = self.ifft(t)    
        t = self.slice(t, [0,0], [self.line, self.iRSampleLen])
        t = t.reshape(t.size)
        return t  

python完整代码示例:

import numpy as np
import mindspore as ms
import mindradar as mr
from mindspore import Tensor, ops, nn
import data_compare as compare

class rpc(nn.Cell):
    def __init__(self):
        super(rpc, self).__init__()
        fFs = 25
        fTp = 210
        fKr = 20.2775 / 210
        iTpADNum = (int)(fFs * fTp + 0.5)
        iRpcLength = 65536
        iRSampleLen = 43456
        iASampleLen = 17
        iChannelNum = 5        
        f_n = iRpcLength - iTpADNum
        pfGSMAngle = np.zeros(iTpADNum, dtype=np.float32)    
        for i in range(iTpADNum):
            tr = (i - iTpADNum / 2.0) / fFs
            pfGSMAngle[i] = np.pi * fKr * tr * tr
        pcGSMData1 = pfGSMAngle.astype(np.complex64)
        pcGSMData1.real = np.cos(pfGSMAngle)
        pcGSMData1.imag = np.sin(pfGSMAngle)
        zero = np.zeros(f_n, dtype=np.complex64)    
        pcGSMData1 = np.append(pcGSMData1, zero, axis=0)
        pcGSMPcRef = np.fft.fft(pcGSMData1).astype(np.complex64)
        pcGSMPcRef = np.conj(pcGSMPcRef)

        tmuls_0 = -(iASampleLen - 1) / 2.0
        fLosVel = 1.485044537103641e+03
        fPRT = 2.38092e+03
        fDr = 5.99584916
        fTemp0 = -fLosVel * fPRT * 1e-6 / fDr / iRpcLength
        pcGSMBuf = np.zeros(iASampleLen, dtype=np.float32)    
        for i in range(iASampleLen):
            pcGSMBuf[i] = (tmuls_0 + i) * fTemp0
        pcGSMRefBuf = pcGSMBuf.astype(np.complex64)
        pcGSMRefBuf.real = np.cos(pcGSMBuf * 2 * np.pi)
        pcGSMRefBuf.imag = np.sin(pcGSMBuf * 2 * np.pi)
        piGSMBuf = np.linspace(-iRpcLength / 2, iRpcLength / 2 - 1, iRpcLength).astype(np.int32)
        line = iASampleLen * iChannelNum
        pcGSMData2 = np.zeros(line * iRpcLength, dtype=np.complex64).reshape(line, iRpcLength)
        for j in range(iASampleLen):
            for i in range(iChannelNum):
                pcGSMData2[j * iChannelNum + i] = np.power(pcGSMRefBuf[j], piGSMBuf)            
        pcGSMRcRef = np.fft.fftshift(pcGSMData2, 1)
        pcGSMRcRef = pcGSMRcRef * pcGSMPcRef
        
        self.pcGSMRcRef = Tensor(pcGSMRcRef)

        self.iASampleLen = iASampleLen
        zero = np.zeros(line*(iRpcLength-iRSampleLen), dtype=np.complex64).reshape(line, iRpcLength-iRSampleLen)
        self.zero = Tensor(zero)
        self.iRpcLength = iRpcLength
        self.iRSampleLen = iRSampleLen
        self.iChannelNum = iChannelNum
        self.line = line
        self.iHeaderCPLX = 256
        self.fft = mr.FFT()
        self.ifft = mr.IFFT()
        self.mul = ops.Mul()
        self.slice = ops.Slice()

    def construct(self, t):
        t = t.reshape(self.iASampleLen, t.size//self.iASampleLen)                 
        t = self.slice(t, [0, self.iHeaderCPLX], [self.iASampleLen, self.iRpcLength*self.iChannelNum])
        t = t.reshape(self.line, self.iRpcLength)
        t = self.slice(t, [0,0], [self.line, self.iRSampleLen])
        t = ops.cat((t, self.zero), 1)
        t = self.fft(t)
        t = self.mul(t, self.pcGSMRcRef)
        t = self.ifft(t)    
        t = self.slice(t, [0,0], [self.line, self.iRSampleLen])
        t = t.reshape(t.size)
        return t  


with open("data_43456_17_rpcdata_chnum5_RPCDataIn.bin", 'rb') as f:
    raw_data = f.read()
echo = np.frombuffer(raw_data, dtype=np.complex64)
echo = Tensor(echo)
fun = rpc()
out = fun(echo)
ms.export(fun, echo, file_name="rpc", file_format="MINDIR")
print('out', out.shape)
print('out', out)

out = out.asnumpy()
compare.data_compare(out, "out_43456_5_17.bin")
compare.data_compare(out, "rpc_model_out.bin")

4. 转换模型

在上述代码中,我们已经使用了 export 方法导出了rpc.mindir模型。 要将该模型部署到 FT04 平台,需要将 mindir 格式转换成 mindspore lite 的 ms 格式。 现使用 MindSpore 的转换工具:converter_lite 进行转换。

转换命令如下:

./converter_lite --fmk=MINDIR --modelFile=/mnt/hgfs/D/work/mind-radar/examples/rpc/rpc.mindir --outputFile=/mnt/hgfs/D/work/mind-radar/examples/rpc/rpc

转换完成后,生成模型rpc.ms 后,可使用可视化工具:netron 打开模型,查看模型结构。

模型结构如图所示:

5. 端侧部署、运行

打开 YHFT-IDE ,新建工程。输入工程名、路径,工程类型选择 Heterogeneous , 输入交叉编译工具路径,然后点确定。会生成一个异构模板工程。

通过修改 data_handler.cc 文件中的函数来调整模型的输入数据,输入改成原始数据文件路径。 在 main 函数设置运行后端;修改 CMakeLists.txt 文件, 最后编译该工程,编译成功后将 build 文件夹下的可执行文件和模型 rpc.ms 一起拷贝到 FT04 中。

在FT04上运行可执行文件,观察输出结果。与预期结果对比,验证模型是否正确。

执行命令如下:

./mindspore_rpc rpc.ms 

程序在执行时会打印部分输入输出数据,以及各部分的运行时间,并生成模型输出的数据文件rpc_model_out.bin,用于后续的验证、分析 。实测该模型的运行时间为 70 ms。

6. 测试

测试验证模型输出结果。

将数据文件rpc_model_out.bin 拷出来, 同标准结果out_43456_5_17.bin 进行比较。 运行结果比较的 python 代码。

compare.data_compare(out, "out_43456_5_17.bin")
compare.data_compare(out, "rpc_model_out.bin")

此处将 python 的输出结果依次与标准结果out_43456_5_17.bin 、模型输出结果rpc_model_out.bin进行比较。

python 输出与标准值的比较结果如下:

python 输出与模型输出值的比较结果如下:
综合分析绝对误差与相对误差,python值与模型输出结果、标准值间的误差很小,均在误差允许范围内。

数据比较代码示例:

import numpy as np  
def data_compare(input, file):  
    real = input.real.astype(np.float32)
    imag = input.imag.astype(np.float32)
    input = np.zeros(real.size * 2, dtype=np.float32)
    for i in range(real.size):
        input[2 * i] = real[i]
        input[2 * i + 1] = imag[i]
    print('input', input.shape)
    print('input', input)

    with open(file, 'rb') as f:
        raw_data = f.read()
    input1 = np.frombuffer(raw_data, dtype=np.float32)
    print('input1', input1.shape)
    print('input1', input1)
    if input.size != input1.size:  
        raise ValueError(f"size: {input.size} != {input1.size}")  
    
    diff = np.abs(input1 - input)  
    diff1 = np.abs(diff/input1)
    max = np.unravel_index(np.argmax(diff), input.shape)  
    max1 = np.unravel_index(np.argmax(diff1), input.shape)  
    # print('diff', diff.shape)
    # print('diff', diff)
    report = "误差分析:\n"  
    report += f"  最大绝对误差: {diff[max]:.6f}\n"  
    report += f"  实测值: {input[max]:.6f}\n"  
    report += f"  标准值: {input1[max]:.6f}\n"  
    report += f"  索引: {max}\n"  
    report += f"  平均误差: {np.mean(diff):.6f}\n"  
    report += f"  误差范围: [{np.min(diff):.6f}, {np.max(diff):.6f}]\n\n"

    report += f"  最大相对误差: {diff1[max1]:.6f}\n"  
    report += f"  实测值: {input[max1]:.6f}\n"  
    report += f"  标准值: {input1[max1]:.6f}\n"  
    report += f"  索引: {max1}\n"  
    report += f"  平均相对误差: {np.mean(diff1):.6f}\n"  
    report += f"  相对误差范围: [{np.min(diff1):.6f}, {np.max(diff1):.6f}]\n\n"

    report += "绝对误差分布直方图 (全范围):\n"  
    bins = np.linspace(0, diff[max], 11)  # 10个区间  
    hist, _ = np.histogram(diff, bins=bins)      
    for i in range(len(bins)-1):  
        start = bins[i]  
        end = bins[i+1]  
        report += f"  误差区间 {start:.3f}-{end:.3f}: {hist[i]}\n"  

    report += "\n相对误差分布直方图 (全范围):\n" 
    bins = np.linspace(0, 0.3, 31)
    bins[19:30] = np.linspace(0.3, 1.3, 11)
    bins[-1] = diff1[max1]
    hist, _ = np.histogram(diff1, bins=bins)      
    for i in range(len(bins)-1):  
        start = bins[i]  
        end = bins[i+1]  
        report += f"  误差区间 {start:.3f}-{end:.3f}: {hist[i]}\n"  

    print(report)  
    return